import os
import warnings
from pathlib import Path
from typing import Any, Dict, Literal

import numpy as np
import pandas as pd
from shapely import convex_hull
from shapely.geometry import (
    LinearRing,
    LineString,
    MultiLineString,
    MultiPoint,
    MultiPolygon,
    Point,
    Polygon,
)
from shapely.ops import nearest_points, polygonize
from shapely.plotting import plot_line, plot_points, plot_polygon

from ..connectors.fileConnector import get_csv_from_stream
from ..connectors.gdimConnector import get_bore_type_mapping
from ..dataclass import GDIDataQualityWarning, GDIWarning
from ..dataclass.geoProfiles import (
    BoreForCadDraw,
    MaterialTable,
    SectionBore,
    SectionForCadDraw,
    SectionHatch,
    SectionInterface,
    SectionLine,
    SectionMaterial,
)
from ..dataclass.tables import FieldMetadata, TableCollection, TableData
from ..dataclass.terminologies import Units
from ..dataTransformers.dataConverter import convert_tables_names
from ..geocal.geoProfileGpro import PreProfile1D, find_node_by_id
from ..geocal.samplesGpro import UniVerticalBores, VerticalBores
from ..pipeline.nameSpace import load_local_function_provider
from ..pipeline.pipeData import (
    NestedRangeModel,
    RangeModel,
    StringAttributeSchema,
    UIAttributeSchema,
)
from ..pipeline.pipeline import (
    PipeModule,
    Port,
    PortReference,
    module_decorator,
    status_manage,
)
from ..pipeline.portTypes import PortType, PortTypeHint


@module_decorator()
class ArithmeticOperator(PipeModule):
    """Calculate two numbers or two number arrays by four rules of arithmetic."""

    # Explicit PortReference annotations - decorator will still handle property creation
    InputNumbers1: PortReference[
        PortTypeHint.Number | PortTypeHint.NumberArray | PortTypeHint.TableSeries
    ]
    InputNumbers2: PortReference[
        PortTypeHint.Number | PortTypeHint.NumberArray | PortTypeHint.TableSeries
    ]
    OutputNumbers: PortReference[
        PortTypeHint.Number | PortTypeHint.NumberArray | PortTypeHint.TableSeries
    ]

    _port_docs = {
        "InputNumbers1": "The first number or number array for the calculation.",
        "InputNumbers2": "The second number or number array for the calculation.",
        "OutputNumbers": "The calculated numbers.",
    }

    def __init__(
        self,
        mname: str | None = "ArithmeticOperator",
        auto_run: bool = True,
        operator: Literal["+", "-", "*", "/"] = "+",
        data1: (
            PortTypeHint.Number
            | PortTypeHint.NumberArray
            | PortTypeHint.TableSeries
            | None
        ) = None,
        data2: (
            PortTypeHint.Number
            | PortTypeHint.NumberArray
            | PortTypeHint.TableSeries
            | None
        ) = None,
    ) -> None:
        """Initialize the ArithmeticOperator object.

        Parameters
        ----------
        operator : Literal["+", "-", "*", "/"], default: "+"
            The operator for the calculation.

        data1 : PortTypeHint.Number | PortTypeHint.NumberArray | None, default: None
            The first number or number array for the calculation.

        data2 : PortTypeHint.Number | PortTypeHint.NumberArray | None, default: None
            The second number or number array for the calculation.
        """
        super().__init__(mname=mname, auto_run=auto_run)

        if data1 is not None:
            self.InputNumbers1 = data1
        if data2 is not None:
            self.InputNumbers2 = data2

        self.operator: str = operator

    def update_ui_schema(self, reset: bool = False) -> dict[str, UIAttributeSchema]:
        return {
            "operator": StringAttributeSchema(
                title="运算符", default="+", selections=["+", "-", "*", "/"]
            )
        }

    def execute(
        self,
    ) -> (
        PortTypeHint.Number | PortTypeHint.NumberArray | PortTypeHint.TableSeries | None
    ):
        data1 = self._ports_in["InputNumbers1"].data
        data2 = self._ports_in["InputNumbers2"].data
        if data1 is None or data2 is None:
            self._ports_out["OutputNumbers"].data = None
            return None

        if self.operator == "+":
            output = data1 + data2
        elif self.operator == "-":
            output = data1 - data2
        elif self.operator == "*":
            output = data1 * data2
        else:
            output = data1 / data2

        self._ports_out["OutputNumbers"].data = output
        return output


@status_manage
class ModifyTableColumns(PipeModule):
    """Modify the values of columns of tables."""

    def __init__(
        self,
        mname: str | None = None,
        auto_run: bool = True,
        table: PortTypeHint.TableData | PortTypeHint.MaterialTable | None = None,
        modified_columns: dict[str, list[Any]] | None = None,
        available_columns: list[str] | None = None,
    ) -> None:
        """Initialize the ModifyTableColumns object.

        Parameters
        ----------
        table : PortTypeHint.TableData | PortTypeHint.MaterialTable | None, default: None
            The table to modify.

        modified_columns : dict[str, list[Any]] | None, default: None
            The names or titles of the columns to modify and their corresponding values.
            Key is the name or title of the column, value is the list of values to set for that column.
            If the length of the values is less than the length of the table rows, the values will be padded with the last value.
            If the length of the values is greater than the length of the table rows, the values will be truncated.

        available_columns : list[str] | None, default: None
            The names the columns that are available to modify.
            If it's not provided, all the columns of the table will be available to modify.
            This is used for UI display.
        """
        super().__init__(mname=mname, auto_run=auto_run)

        self._ports_in = {
            "InputTable": Port(
                ptype=[PortType.TableData, PortType.MaterialTable],
                data=table,
                pdoc="The table to modify.",
            )
        }
        self._ports_out = {
            "OutputTable": Port(
                ptype=[PortType.TableData, PortType.MaterialTable],
                pdoc="The modified table.",
            )
        }

        self.modified_columns = modified_columns
        self.available_columns = available_columns

    @property
    def InputTable(self) -> None:
        raise AttributeError("Property 'InputTable' is write-only.")

    @InputTable.setter
    def InputTable(
        self, value: PortTypeHint.TableData | PortTypeHint.MaterialTable | None
    ) -> None:
        self._ports_in["InputTable"].data = value

    @property
    def OutputTable(self) -> PortTypeHint.TableData | PortTypeHint.MaterialTable | None:
        return self._ports_out["OutputTable"].data

    def set_cal_params(self, reset: bool = True) -> Dict[str, RangeModel]:
        table: TableData | MaterialTable | None = self._ports_in["InputTable"].data
        if table is None:
            dict_key_choices = None
            dict_key_choices_label = None
        else:
            if self.available_columns is None:
                dict_key_choices = table.columns
                dict_key_choices_label = table.field_titles
            else:
                dict_key_choices = self.available_columns
                name_to_title = table.name_to_title
                dict_key_choices_label = [
                    name_to_title[col] for col in self.available_columns
                ]

        return {
            "modified_columns": RangeModel(
                vtype="dict",
                title="修改的列",
                default=self.modified_columns,
                dict_value_type="list",
                dict_key_choices=dict_key_choices,
                dict_key_choices_label=dict_key_choices_label,
            )
        }

    def execute(self) -> PortTypeHint.TableData | PortTypeHint.MaterialTable | None:
        table: TableData | MaterialTable | None = self._ports_in["InputTable"].data
        if table is None:
            self._ports_out["OutputTable"].data = None
            return None

        if self.modified_columns is None or len(self.modified_columns) == 0:
            # No changes are made to the table
            self._ports_out["OutputTable"].data = table
            return table

        # Copy the table to avoid modifying the original
        output_table = table.copy()

        # Modify each specified column
        for column_name, column_values in self.modified_columns.items():
            if (
                column_name in output_table.columns
                or column_name in output_table.field_titles
            ):
                if isinstance(column_values, list):
                    values_array = np.array(column_values)
                    # Pad with last value if too short
                    if len(values_array) < len(output_table):
                        values_array = np.pad(
                            values_array,
                            (0, len(output_table) - len(values_array)),
                            mode="edge",
                        )
                    # Truncate if too long
                    elif len(values_array) > len(output_table):
                        values_array = values_array[: len(output_table)]
                    output_table[column_name] = values_array
                else:
                    # Single value for all rows
                    output_table[column_name] = [column_values] * len(output_table)

        self._ports_out["OutputTable"].data = output_table
        return output_table


@status_manage
class ModifyTablesColumns(PipeModule):
    """Modify the values of columns of tables."""

    def __init__(
        self,
        mname: str | None = None,
        auto_run: bool = True,
        tables: PortTypeHint.TableCollection | None = None,
        column_names: dict[str, list[str]] | None = None,
        column_values: dict[str, list[list[Any]]] | None = None,
    ) -> None:
        """Initialize the ModifyTablesColumns object.

        Parameters
        ----------
        tables : PortTypeHint.TableCollection | None, default: None
            The tables to modify.

        column_names : dict[str, list[str]] | None, default: None
            The names or titles of the columns to modify.
            Key is the name or title of the table, value is the names or titles of the columns of each table to modify.

        column_values: dict[str, list[list[Any]]] | None, default: None
            The values to modify the columns.
            Key is the name or title of the table, value is the values to modify the columns of each table.
            If the length of the values is less than the length of the columns to modify, the values will be padded with the last value.
            If the length of the values is greater than the length of the columns to modify, the values will be truncated.
        """
        super().__init__(mname=mname, auto_run=auto_run)

        self._ports_in = {
            "InputTables": Port(
                ptype=PortType.TableCollection,
                data=tables,
                pdoc="The tables to modify.",
            )
        }
        self._ports_out = {
            "OutputTables": Port(
                ptype=PortType.TableCollection, pdoc="The modified tables."
            )
        }

        self.column_names = column_names
        self.column_values = column_values

    @property
    def InputTables(self) -> None:
        raise AttributeError("Property 'InputTables' is write-only.")

    @InputTables.setter
    def InputTables(self, value: PortTypeHint.TableCollection | None) -> None:
        self._ports_in["InputTables"].data = value

    @property
    def OutputTables(self) -> PortTypeHint.TableCollection | None:
        return self._ports_out["OutputTables"].data

    def set_cal_params(self, reset: bool = False) -> Dict[str, RangeModel]:
        pass

    def execute(self) -> PortTypeHint.TableCollection | None:
        tables = self._ports_in["InputTables"].data
        if tables is None:
            self._ports_out["OutputTables"].data = None
            return None

        if (
            self.column_names is None
            or len(self.column_names) == 0
            or self.column_values is None
            or len(self.column_values) == 0
        ):
            # No changes are made to the tables
            self._ports_out["OutputTables"].data = tables
            return tables

        output_tables = TableCollection()

        for table_name, column_names in self.column_names.items():
            if table_name in tables:
                table = tables[table_name]
                # copy the table
                output_table: TableData = table.copy()
                # get each column in the table and modify it
                column_values = self.column_values.get(table_name)
                if column_values:
                    for idx, column_name in enumerate(column_names):
                        if (
                            column_name in table.columns
                            or column_name in table.field_titles
                        ):
                            column_values = column_values[idx]
                            if len(column_values) < len(output_table):
                                # Pad with last value if too short
                                column_values = np.pad(
                                    column_values,
                                    (0, len(output_table) - len(column_values)),
                                    mode="edge",
                                )
                            elif len(column_values) > len(output_table):
                                # Truncate if too long
                                column_values = column_values[: len(output_table)]
                            output_table[column_name] = column_values
                output_tables.add_table(output_table)

        self._ports_out["OutputTables"].data = output_tables
        return output_tables


@module_decorator()
class AddTableColumns(PipeModule):
    """Add columns to tables."""

    InputTable: PortReference[PortTypeHint.TableData]
    OutputTable: PortReference[PortTypeHint.TableData]

    _port_docs = {
        "InputTable": "The table to add columns to.",
        "OutputTable": "The table with added columns.",
    }

    # Define supported arithmetic operation types
    ARITHMETIC_OPERATIONS = {
        "+": lambda x, y: x + y,
        "-": lambda x, y: x - y,
        "*": lambda x, y: x * y,
        "/": lambda x, y: x / y,
        "//": lambda x, y: x // y,  # Floor division
        "%": lambda x, y: x % y,  # Modulo
        "**": lambda x, y: x**y,  # Power
    }

    def __init__(
        self,
        mname: str = "AddTableColumns",
        auto_run: bool = True,
        table: PortTypeHint.TableData | None = None,
        column_names: list[FieldMetadata] | list[dict[str, str]] | None = None,
        column_values: dict[str, Any] | None = None,
        column_calculations: dict[str, dict[str, str]] | None = None,
        local_functions_path: str | None = None,
        local_function_name: str | None = None,
    ) -> None:
        """Initialize the AddTableColumns object.

        Parameters
        ----------
        table : PortTypeHint.TableData | None, default: None
            The table to add columns to.

        column_names : list[FieldMetadata] | list[dict[str, str]] | None, default: None
            The metadata of the columns to add.

        column_values : dict[str, Any] | None, default: None
            The values of the added columns and existing columns.
            key is the name of the column, value is the values of the columns.
            If the value is not a `list` or `np.ndarray`, it's a single value for all rows.
            If column_values are not provided, `NaN` vaule will be added to the added columns, and the existing columns will be unchanged.

        column_calculations : dict[str, dict[str, str]] | None, default: None
            The calculations to perform for new columns based on existing columns.
            Key is the name of the new column, value is a dictionary with:
            - "operand1": name or title of the first column
            - "operator": arithmetic operator ("+", "-", "*", "/", "//", "%", "**")
            - "operand2": name or title of the second column or a numeric value
            Example: {"new_col": {"operand1": "col1", "operator": "+", "operand2": "col2"}}
            If specified, takes precedence over column_values for that column.

        local_functions_path : str | None, default: None
            The path to the local functions file.
            If it's not provided, the module will try to use the local functions file path in the pipeline.

        local_function_name : str | None, default: None
            The name of the class that defines the function.
            This function is used for calculating RangeModel for column values.
        """
        super().__init__(mname=mname, auto_run=auto_run)

        if table is None:
            self.InputTable = table

        if column_names:
            if isinstance(column_names[0], FieldMetadata):
                column_names = [
                    {"name": field.name, "title": field.title, "unit": field.unit}
                    for field in column_names
                ]
        self._column_names = column_names
        self.column_values = column_values
        self.column_calculations = column_calculations
        self.local_functions_path = local_functions_path
        self.local_function_name = local_function_name

    @property
    def column_names(self) -> list[dict[str, str]] | None:
        return self._column_names

    @column_names.setter
    def column_names(
        self, value: list[FieldMetadata] | list[dict[str, str]] | None
    ) -> None:
        if value:
            if isinstance(value[0], FieldMetadata):
                value = [
                    {"name": field.name, "title": field.title, "unit": field.unit}
                    for field in value
                ]
        self._column_names = value

    def set_cal_params(
        self, reset: bool = False
    ) -> dict[str, RangeModel | NestedRangeModel]:
        # Get columns_values RangeModel from the local function
        local_function_exists = False
        if self.local_functions_path and self.local_function_name:
            local_function_exists = True
            local_functions_path = self.local_functions_path
        # Try to get the local function path from the pipeline if self.local_functions_path is not provided
        elif (
            self.pipeline
            and self.pipeline.local_functions_path
            and self.local_function_name
        ):
            local_function_exists = True
            local_functions_path = self.pipeline.local_functions_path

        if local_function_exists:
            values_range_class = load_local_function_provider(
                local_functions_path, self.local_function_name
            )
            # Get the function context of the class
            function_context = values_range_class.context
            for key in function_context.keys():
                if key in self.keys()["input_ports"]:
                    value = self._ports_in[key].data
                elif key in self.keys()["output_ports"]:
                    value = self._ports_out[key].data
                else:
                    try:
                        value = getattr(self, key)
                    except AttributeError:
                        raise ValueError(
                            f"The attribute '{key}' is not found in the current module '{self.mname}'."
                        )
                function_context[key] = value
            # Run the local function
            columns_values_range = values_range_class.run_function()
            if not isinstance(columns_values_range, NestedRangeModel):
                raise ValueError(
                    f"The local function '{self.local_function_name}' must return a 'NestedRangeModel' object."
                )
        else:
            fields = {}
            for column in self.column_names:
                fields[column["name"]] = RangeModel(vtype="Any", title=column["title"])
            columns_values_range = NestedRangeModel(
                vtype="dict", fields=fields, title="默认值", required=False
            )

        values_range = {"column_values": columns_values_range}

        return values_range

    def _get_calculation_function(self, calc_config: dict) -> callable:
        """Return the appropriate calculation function based on configuration."""
        operator = calc_config["operator"]

        if operator in self.ARITHMETIC_OPERATIONS:
            operation_func = self.ARITHMETIC_OPERATIONS[operator]
            return lambda x, y: operation_func(x, y)
        else:
            raise ValueError(f"The arithmetic operator '{operator}' is not supported.")

    def _calculate_column_values(
        self, table: PortTypeHint.TableData, column_name: str, calc_config: dict
    ) -> list:
        """Calculate column values based on arithmetic operations."""
        operand1_name = calc_config["operand1"]
        operand2_name = calc_config["operand2"]

        # Get operand1 data (must be a column)
        if operand1_name in table.columns:
            operand1_data = table[operand1_name]
        elif operand1_name in table.field_titles:
            # Convert title to column name
            title_to_name = table.title_to_name
            operand1_data = table[title_to_name[operand1_name]]
        else:
            raise ValueError(
                f"Column '{operand1_name}' not found in table for calculation of '{column_name}'."
            )

        # Get operand2 data (can be a column or numeric value)
        try:
            # Try to convert to numeric value first
            operand2_data = float(operand2_name)
        except (ValueError, TypeError):
            # If not numeric, treat as column name/title
            if operand2_name in table.columns:
                operand2_data = table[operand2_name]
            elif operand2_name in table.field_titles:
                # Convert title to column name
                title_to_name = table.title_to_name
                operand2_data = table[title_to_name[operand2_name]]
            else:
                raise ValueError(
                    f"Column or numeric value '{operand2_name}' not found for calculation of '{column_name}'."
                )

        # Get the calculation function
        calc_func = self._get_calculation_function(calc_config)

        # Perform the calculation
        try:
            result = calc_func(operand1_data, operand2_data)
            # Convert to list if it's a pandas Series
            if hasattr(result, "tolist"):
                return result.tolist()
            elif isinstance(result, (list, np.ndarray)):
                return list(result)
            else:
                # Single value for all rows
                return [result] * len(table)
        except Exception as e:
            raise ValueError(f"Error calculating column '{column_name}': {str(e)}")

    def execute(self) -> PortTypeHint.TableData | None:
        table = self._ports_in["InputTable"].data
        if table is None:
            self._ports_out["OutputTable"].data = None
            return None

        if self.column_names is None or len(self.column_names) == 0:
            self._ports_out["OutputTable"].data = table
            return table

        # Copy the table to avoid modifying the original
        output_table: TableData = table.copy()
        if isinstance(output_table, MaterialTable):
            output_table = output_table.convert_to_table_data()

        # Change values of existing columns
        if self.column_values:
            for column_name, column_values in self.column_values.items():
                if column_name in output_table.columns:
                    if isinstance(column_values, (list, np.ndarray)):
                        output_table[column_name] = column_values
                    else:
                        output_table[column_name] = [column_values] * len(output_table)

        # Add new columns based on column_names metadata
        for field_metadata in self.column_names:
            column_name = field_metadata["name"]

            # Check if this column has a calculation defined
            if self.column_calculations and column_name in self.column_calculations:
                # Calculate values based on arithmetic operations
                calc_config = self.column_calculations[column_name]
                column_values = self._calculate_column_values(
                    output_table, column_name, calc_config
                )
            elif self.column_values and column_name in self.column_values:
                # Use provided values
                column_values = self.column_values[column_name]

                # If the value is a list or numpy array, use it directly
                if isinstance(column_values, (list, np.ndarray)):
                    column_values = np.array(column_values)
                    # Pad with last value if too short
                    if len(column_values) < len(output_table):
                        column_values = np.pad(
                            column_values,
                            (0, len(output_table) - len(column_values)),
                            mode="edge",
                        )
                    # Truncate if too long
                    elif len(column_values) > len(output_table):
                        column_values = column_values[: len(output_table)]
                else:
                    # Single value for all rows
                    column_values = [column_values] * len(output_table)
            else:
                # No default value provided, use NaN
                column_values = [np.nan] * len(output_table)

            # Add the column to the table
            output_table[column_name] = column_values

        # Update field metadata for all new columns
        output_table.update_fields_meta(self.column_names)

        self._ports_out["OutputTable"].data = output_table
        return output_table


@status_manage
class AddTables(PipeModule):
    """Add tables to an existing table collection."""

    def __init__(
        self,
        mname: str | None = None,
        auto_run: bool = True,
        tables_to_add: (
            PortTypeHint.TableCollection | PortTypeHint.TableData | None
        ) = None,
        tables_added_to: PortTypeHint.TableCollection | None = None,
        main_table_name: str | None = None,
        sub_table_names: list[str] | None = None,
        main_key: str | None = None,
    ) -> None:
        """
        Initialize the AddTables object.

        Parameters
        ----------
        tables_to_add: PortTypeHint.TableCollection | PortTypeHint.TableData | None, default: None
            The tables to add to the collection.

        tables_added_to: PortTypeHint.TableCollection | None, default: None
            The collection to add the tables to.

        main_table_name: str | None, default: None
            The name of the main table.
            If we want to reset the name of the main table in the collection.

        sub_table_names: list[str] | None, default: None
            The names of the sub tables.
            If we want to reset the name of the sub tables in the collection.

        main_key: str | None, default: None
            The main key of the main table.
            If we want to reset the main key of the main table in the collection.

        Note
        ----
        If "InputTablesToAdd" is None, the "OutputTables" will be the same as "InputTablesAddedTo".
        If "InputTablesAddedTo" is None, the "OutputTables" will be a new TableCollection and the "InputTablesToAdd" will be added to it.
        """
        super().__init__(mname=mname, auto_run=auto_run)

        self._ports_in = {
            "InputTablesToAdd": Port(
                ptype=[PortType.TableCollection, PortType.TableData],
                data=tables_to_add,
                pdoc="The tables to add to the collection.",
            ),
            "InputTablesAddedTo": Port(
                ptype=PortType.TableCollection,
                data=tables_added_to,
                pdoc="The collection to add the tables to.",
            ),
        }
        self._ports_out = {
            "OutputTables": Port(
                ptype=PortType.TableCollection,
                pdoc="The resulting table collection after adding tables.",
            )
        }

        self.main_table_name = main_table_name
        self.sub_table_names = sub_table_names
        self.main_key = main_key

    @property
    def InputTablesToAdd(self) -> None:
        raise AttributeError("Property 'InputTablesToAdd' is write-only.")

    @InputTablesToAdd.setter
    def InputTablesToAdd(
        self, value: PortTypeHint.TableCollection | PortTypeHint.TableData | None
    ) -> None:
        self._ports_in["InputTablesToAdd"].data = value

    @property
    def InputTablesAddedTo(self) -> None:
        raise AttributeError("Property 'InputTablesAddedTo' is write-only.")

    @InputTablesAddedTo.setter
    def InputTablesAddedTo(self, value: PortTypeHint.TableCollection | None) -> None:
        self._ports_in["InputTablesAddedTo"].data = value

    @property
    def OutputTables(self) -> PortTypeHint.TableCollection | None:
        return self._ports_out["OutputTables"].data

    def set_cal_params(self, reset: bool = True) -> None:
        return None

    def execute(self) -> PortTypeHint.TableCollection | None:
        tables_to_add = self._ports_in["InputTablesToAdd"].data
        tables_added_to = self._ports_in["InputTablesAddedTo"].data

        if tables_to_add is None:
            self._ports_out["OutputTables"].data = tables_added_to
            return tables_added_to

        if tables_added_to is None:
            tables_added_to = TableCollection()

        # Create a copy to avoid modifying the original
        output_collection = tables_added_to.copy()

        # Handle single TableData
        if isinstance(tables_to_add, TableData):
            output_collection.add_table(tables_to_add)
        # Handle TableCollection
        elif isinstance(tables_to_add, TableCollection):
            for table in tables_to_add:
                output_collection.add_table(table)
        else:
            raise ValueError("Input tables must be a TableData or TableCollection")

        if self.main_table_name is not None:
            output_collection.main_table = self.main_table_name

        if self.sub_table_names is not None:
            output_collection.sub_tables = self.sub_table_names

        if self.main_key is not None:
            output_collection.main_key = self.main_key

        self._ports_out["OutputTables"].data = output_collection
        return output_collection


@status_manage
class RenameTableColumns(PipeModule):
    """Rename table columns and field titles while updating fields metadata."""

    def __init__(
        self,
        mname: str | None = None,
        auto_run: bool = True,
        table: PortTypeHint.TableData | None = None,
        column_name_map: dict[str, str] | None = None,
        rename_type: Literal["name", "title"] = "name",
    ) -> None:
        """Initialize the RenameTableColumns object.

        Parameters
        ----------
        table : PortTypeHint.TableData | None, default: None
            The table to rename columns.

        column_name_map : dict[str, str] | None, default: None
            The mapping for renaming.
            If rename_type="name": {old_column_name: new_column_name}
            If rename_type="title": {old_field_title: new_field_title}

        rename_type : Literal["name", "title"], default: "name"
            "name": rename column names, "title": rename field titles
        """
        super().__init__(mname=mname, auto_run=auto_run)

        self._ports_in = {
            "InputTable": Port(
                ptype=PortType.TableData,
                data=table,
                pdoc="The table to rename columns.",
            )
        }
        self._ports_out = {
            "OutputTable": Port(
                ptype=PortType.TableData, pdoc="The table with renamed columns."
            )
        }

        self.column_name_map = column_name_map
        self.rename_type = rename_type

    @property
    def InputTable(self) -> None:
        raise AttributeError("Property 'InputTable' is write-only.")

    @InputTable.setter
    def InputTable(self, value: PortTypeHint.TableData | None) -> None:
        self._ports_in["InputTable"].data = value

    @property
    def OutputTable(self) -> PortTypeHint.TableData | None:
        return self._ports_out["OutputTable"].data

    def set_cal_params(self, reset: bool = True) -> dict[str, RangeModel]:
        table: TableData | None = self._ports_in["InputTable"].data
        if table is None:
            dict_key_choices = None
        else:
            if self.rename_type == "title":
                dict_key_choices = table.field_titles
            else:
                dict_key_choices = table.columns.tolist()

        title = "字段标题映射" if self.rename_type == "title" else "字段名称映射"
        return {
            "column_name_map": RangeModel(
                vtype="dict",
                title=title,
                default=self.column_name_map,
                dict_value_type="str",
                dict_key_choices=dict_key_choices,
                depends_on="rename_type",
            ),
            "rename_type": RangeModel(
                vtype="str",
                title="映射类型",
                choices=["name", "title"],
                default=self.rename_type,
            ),
        }

    def execute(self) -> PortTypeHint.TableData | None:
        table: TableData | None = self._ports_in["InputTable"].data
        if table is None:
            self._ports_out["OutputTable"].data = None
            return None

        if self.column_name_map is None or len(self.column_name_map) == 0:
            # No renaming needed
            self._ports_out["OutputTable"].data = table
            return table

        # Copy the table to avoid modifying the original
        output_table: TableData = table.copy()

        if self.rename_type == "name":
            # Rename column names: {old_column_name: new_column_name}
            rename_map = {}
            for old_col_name, new_col_name in self.column_name_map.items():
                if old_col_name in output_table.columns:
                    rename_map[old_col_name] = new_col_name
                else:
                    warnings.warn(
                        f"Column name '{old_col_name}' not found in table, skipping."
                    )

            # Rename the columns in the DataFrame
            if rename_map:
                output_table.rename(columns=rename_map, inplace=True)

            # Update fields metadata to match new column names
            # We need to update both the field metadata names AND the dictionary keys
            if hasattr(output_table, "fields_meta") and output_table.fields_meta:
                new_fields_meta = {}
                for old_col_name, field_meta in output_table.fields_meta.items():
                    if old_col_name in rename_map:
                        new_col_name = rename_map[old_col_name]
                        # Create new FieldMetadata with updated name
                        new_field_meta = FieldMetadata(
                            name=new_col_name,
                            title=field_meta.title,
                            unit=field_meta.unit,
                            description=field_meta.description,
                        )
                        # Use new column name as dictionary key
                        new_fields_meta[new_col_name] = new_field_meta
                    else:
                        # Keep unchanged metadata
                        new_fields_meta[old_col_name] = field_meta

                # Replace the fields_meta dictionary
                output_table._fields_meta = new_fields_meta

                # Update title_to_name mapping
                new_title_to_name = {}
                for col_name, field_meta in new_fields_meta.items():
                    if field_meta.title:
                        new_title_to_name[field_meta.title] = col_name
                output_table._title_to_name = new_title_to_name

        else:
            # Rename field titles: {old_field_title: new_field_title}
            if hasattr(output_table, "fields_meta") and output_table.fields_meta:
                new_fields_meta = {}
                for col_name, field_meta in output_table.fields_meta.items():
                    if field_meta.title in self.column_name_map:
                        new_title = self.column_name_map[field_meta.title]
                        # Create new FieldMetadata with updated title
                        new_field_meta = FieldMetadata(
                            name=field_meta.name,
                            title=new_title,
                            unit=field_meta.unit,
                            description=field_meta.description,
                        )
                        new_fields_meta[col_name] = new_field_meta
                    elif (
                        field_meta.title is None
                        and field_meta.name in self.column_name_map
                    ):
                        # Fallback: if title is None, try to match by name
                        new_title = self.column_name_map[field_meta.name]
                        new_field_meta = FieldMetadata(
                            name=field_meta.name,
                            title=new_title,
                            unit=field_meta.unit,
                            description=field_meta.description,
                        )
                        new_fields_meta[col_name] = new_field_meta
                    else:
                        # Keep unchanged metadata
                        new_fields_meta[col_name] = field_meta

                # Replace the fields_meta dictionary
                output_table._fields_meta = new_fields_meta

                # Update title_to_name mapping
                new_title_to_name = {}
                for col_name, field_meta in new_fields_meta.items():
                    if field_meta.title:
                        new_title_to_name[field_meta.title] = col_name
                output_table._title_to_name = new_title_to_name

                # Check for titles that weren't found
                found_titles = {
                    meta.title
                    for meta in output_table.fields_meta.values()
                    if meta.title
                }
                for old_title in self.column_name_map.keys():
                    if (
                        old_title not in found_titles
                        and old_title not in output_table.columns
                    ):
                        warnings.warn(
                            f"Field title '{old_title}' not found in table, skipping."
                        )

        self._ports_out["OutputTable"].data = output_table
        return output_table


@status_manage
class TablesNamesMapper(PipeModule):
    """Convert the names of tables or fields by the mapping file"""

    def __init__(
        self,
        mname: str | None = "TablesNamesMapper",
        auto_run: bool = True,
        tables: PortTypeHint.TableCollection | None = None,
        gdim_file: PortTypeHint.GdimFile | None = None,
        mapping_file_path: str | Path | None = None,
        mapping_file_name: str | None = None,
        pipeline_workspace_as_file_path: bool = True,
    ) -> None:
        """
        Initialize TablesNamesMapper object.

        Parameters
        ----------
        tables: PortTypeHint.TableCollection | None, default: None
            The tables input.

        gdim_file: PortTypeHint.GdimFile | None, default: None
            The gdim file-csv input.

        mapping_file_path: str | Path | None, default: None
            The path to the mapping file.

        mapping_file_name: str | None, default: None
            The name of the mapping file.

        pipeline_workspace_as_file_path: bool, default: True
            If True, the workspace of the pipeline will be used as the file path at first.
            If False, the mapping_file_path parameter will be used as the file path at first.
        """
        super().__init__(mname=mname, auto_run=auto_run)

        self._ports_in = {
            "InputTables": Port(
                ptype=PortType.TableCollection, data=tables, pdoc="The tables input."
            ),
            "InputGdimFile": Port(
                ptype=PortType.GdimFile, data=gdim_file, pdoc="The gdim file-csv input."
            ),
        }
        self._ports_out = {
            "OutputTables": Port(
                ptype=PortType.TableCollection, pdoc="The tables output. "
            )
        }

        self.mapping_file_path = mapping_file_path
        self.mapping_file_name = mapping_file_name
        self.pipeline_workspace_as_file_path = pipeline_workspace_as_file_path

    @property
    def InputTables(self) -> None:
        raise AttributeError("Property 'InputTables' is write-only.")

    @InputTables.setter
    def InputTables(self, value: PortTypeHint.TableCollection | None) -> None:
        self._ports_in["InputTables"].data = value

    @property
    def InputGdimFile(self) -> None:
        raise AttributeError("Property 'InputGdimFile' is write-only.")

    @InputGdimFile.setter
    def InputGdimFile(self, value: PortTypeHint.GdimFile | None) -> None:
        self._ports_in["InputGdimFile"].data = value

    @property
    def OutputTables(self) -> PortTypeHint.TableData | None:
        return self._ports_out["OutputTables"].data

    def set_cal_params(self, reset: bool = True) -> dict[str, RangeModel]:
        return {
            "mapping_file_name": RangeModel(
                vtype="str", default=self.mapping_file_name, title="映射文件名"
            )
        }

    def execute(self) -> PortTypeHint.TableCollection | None:
        tables: PortTypeHint.TableCollection | None = self._ports_in["InputTables"].data
        gdim_file: PortTypeHint.GdimFile | None = self._ports_in["InputGdimFile"].data

        if tables is None:
            self._ports_out["OutputTables"].data = None
            return None

        if gdim_file:  # Get the mapping file from the gdim minIO server
            url = gdim_file.get_download_url()
            mapping_df = get_csv_from_stream(url)
            if not isinstance(mapping_df, pd.DataFrame):
                raise Exception("映射文件数据解析失败!")

            tables_destination = convert_tables_names(
                tables_original=tables, map_tables_df=mapping_df
            )
            self._ports_out["OutputTables"].data = tables_destination
            return tables_destination

        if self.mapping_file_name is None:
            self._ports_out["OutputTables"].data = None
            return None

        if self.pipeline_workspace_as_file_path:
            # Try to find the file_path from the pipeline at first
            if self.pipeline and self.pipeline.workspace:
                file_path = self.pipeline.workspace
            elif self.mapping_file_path:
                file_path = self.mapping_file_path
            else:
                file_path = os.getcwd()
        else:
            # Try to find the file_path from the module at first
            if self.mapping_file_path:
                file_path = self.mapping_file_path
            elif self.pipeline and self.pipeline.workspace:
                file_path = self.pipeline.workspace
            else:
                file_path = os.getcwd()

        mapping_file_path = Path(file_path)
        mapping_file = mapping_file_path / self.mapping_file_name

        tables_destination = convert_tables_names(
            tables_original=tables, map_file=mapping_file
        )
        self._ports_out["OutputTables"].data = tables_destination
        return tables_destination


####################
# GDIM Specific
####################


@module_decorator()
class AddLayerNumberByStdLayerTable(PipeModule):
    """Add `layer_number` to the `layer_table` by the `layer_number` data from `standard_layer_table`.
    This used when there is no `layer_number` column in the `layer_table`.
    """

    InputTables: PortReference[PortTypeHint.TableCollection]
    InputTemplate: PortReference[PortTypeHint.GdimTemplate]
    OutputTables: PortReference[PortTypeHint.TableCollection]

    _port_docs = {
        "InputTables": "The tables to add layer number to.",
        "InputTemplate": "The template to add layer number to.",
        "OutputTables": "The tables with added layer number.",
    }

    def __init__(
        self,
        mname: str = "AddLayerNumberByStdLayerTable",
        auto_run: bool = True,
        tables: PortTypeHint.TableCollection | None = None,
        template: PortTypeHint.GdimTemplate | None = None,
        layer_table_name: str | None = None,
        layer_number_name: str | None = None,
        layer_number_title: str | None = None,
    ) -> None:
        """Initialize AddLayerNumberByStdLayerTable object.

        Parameters
        ----------
        tables: PortTypeHint.TableCollection | None, default: None
            The tables to add `layer_number` to.

        template: PortTypeHint.GdimTemplate | None, default: None
            The template to add `layer_number` to.

        layer_table_name: str | None, default: None
            The name or title of the `layer_table`.
            If None, `layer_table` will be used as the name.

        layer_number_name: str | None, default: None
            The name of the `layer_number` column.
            If None, `layer_number` will be used as the name of the new column added into the `layer_table`.

        layer_number_title: str | None, default: None
            The title of the `layer_number` column.
            If None, `地层编号` will be used as the title of the new column added into the `layer_table`.

        Note
        ----
        For the `standard_layer_table`, it must be named as `standard_layer_table` in the input table collection.
        The layer number column must be named as `layer_number` in the `standard_layer_table`.
        The `standard_layer_table` and `layer_table` must be provided in the input table collection.

        If a column with the same name or title of the provided `layer_number_name` or `layer_number_title` is found in the layer table,
        the `layer_number` will not be added to the column.
        """
        super().__init__(mname=mname, auto_run=auto_run)

        if tables is not None:
            self.InputTables = tables
        if template is not None:
            self.InputTemplate = template

        self.layer_table_name = layer_table_name
        self.layer_number_name = layer_number_name
        self.layer_number_title = layer_number_title

        self._title = "添加地层编号"
        self._description = "根据标准地层及其自动确定地层的字段名称，为没有地层编号的地层表添加地层编号列。适用于GDIM中标准地层配置中选择自动确定地层字段时的情况。"

    def execute(self) -> PortTypeHint.TableCollection | None:
        tables: PortTypeHint.TableCollection | None = self._ports_in["InputTables"].data
        template: PortTypeHint.GdimTemplate | None = self._ports_in[
            "InputTemplate"
        ].data

        if tables is None or template is None:
            self._ports_out["OutputTables"].data = None
            return None

        if self.layer_table_name is None:
            layer_table_name = "layer_table"
        else:
            layer_table_name = self.layer_table_name

        if self.layer_number_name is None:
            layer_number_name = "layer_number"
        else:
            layer_number_name = self.layer_number_name

        if self.layer_number_title is None:
            layer_number_title = "地层编号"
        else:
            layer_number_title = self.layer_number_title

        # Check whether there is a layer table and standard layer table in the input table collection
        layer_table = tables.get_table(layer_table_name)
        if layer_table is None or layer_table.empty:
            raise ValueError(
                f"'{layer_table_name}' is not found or empty in the input table collection."
            )
        standard_layer_table = tables.get_table("standard_layer_table")
        if standard_layer_table is None or standard_layer_table.empty:
            raise ValueError(
                f"'standard_layer_table' is not found or empty in the input table collection."
            )

        # Check whether the layer table already has the layer number column
        if (
            layer_number_name in layer_table.field_names
            or layer_number_title in layer_table.field_titles
        ):
            warnings.warn(
                f"'{layer_number_name}' is already in the layer table, no change will be made to the input table collection."
            )
            self._ports_out["OutputTables"].data = tables
            return tables

        # Add layer number to the layer table
        standard_layer_update = template.standard_layer_update
        if standard_layer_update is None:
            raise ValueError(
                "The standard layer update method is not found in the template."
            )
        update_method = standard_layer_update.update_method
        if update_method != "autoGeneration":
            warnings.warn(
                "The standard layer update method is not autoGeneration, no change will be made to the input table collection."
            )
            self._ports_out["OutputTables"].data = tables
            return tables

        define_layer_fields = standard_layer_update.define_layer_fields

        if not define_layer_fields:
            raise ValueError("No define_layer_fields found in the template.")

        # Find common fields between define_layer_fields and layer_table
        layer_table_fields = layer_table.field_names  # Only find the same field names
        common_fields = []

        for field in define_layer_fields:
            if field in layer_table_fields:
                common_fields.append(field)

        if not common_fields:
            raise ValueError(
                f"No common fields found between define_layer_fields {define_layer_fields} "
                f"and layer_table fields {list(layer_table_fields)}."
            )

        # Check if layer_number column exists in standard_layer_table
        if "layer_number" not in standard_layer_table.field_names:
            raise ValueError("'layer_number' column not found in standard_layer_table.")

        # Use pandas merge for efficient join operation
        # Create a merge operation on common_fields to get layer_number
        merged_result = layer_table.merge(
            standard_layer_table[common_fields + ["layer_number"]],
            on=common_fields,
            how="left",
        )

        # Check for any missing layer numbers (NaN values)
        missing_mask = merged_result["layer_number"].isna()
        if missing_mask.any():
            # Get the first missing row for error reporting
            first_missing_idx = missing_mask.idxmax()
            missing_values = tuple(
                merged_result.loc[first_missing_idx, common_fields].values
            )
            raise ValueError(
                f"对于值为 {missing_values} 的地层，没有在标准地层中找到对应的地层编号，或地层编号为空"
            )

        # Add the new layer_number column to layer_table
        layer_table[layer_number_name] = merged_result["layer_number"]

        # Add field metadata for the new column
        layer_table.update_field_metadata(
            field_name=layer_number_name,
            title=layer_number_title,
            unit=Units.UNITLESS,
            description=f"Layer number added from standard_layer_table",
        )

        self._ports_out["OutputTables"].data = tables
        return tables


@module_decorator()
class MapBoreTypeToKCAD(PipeModule):
    """Map the value of `bore_type` in the `bore_table` to standard bore type in KCAD."""

    InputTables: PortReference[PortTypeHint.TableCollection]
    InputToken: PortReference[PortTypeHint.Token]
    OutputTables: PortReference[PortTypeHint.TableCollection]

    _port_docs = {
        "InputTables": "The tables to map bore type to KCAD.",
        "InputToken": "The token to access the KCAD.",
        "OutputTables": "The tables with mapped bore type to KCAD.",
    }

    def __init__(
        self,
        mname: str = "MapBoreTypeToKCAD",
        auto_run: bool = True,
        tables: PortTypeHint.TableCollection | None = None,
        token: PortTypeHint.Token | None = None,
    ) -> None:
        """Initialize MapBoreTypeToKCAD object.

        Parameters
        ----------
        tables: PortTypeHint.TableCollection | None, default: None
            The tables to map bore type to KCAD.

        token: PortTypeHint.Token | None, default: None
            The token and proj_id.
        """
        super().__init__(mname=mname, auto_run=auto_run)

        if tables is not None:
            self.InputTables = tables
        if token is not None:
            self.InputToken = token

        self._title = "映射钻孔类型"
        self._description = "将钻孔一览表中的钻孔类型映射为KCAD标准钻孔类型。"

    def execute(self) -> PortTypeHint.TableCollection | None:
        tables: PortTypeHint.TableCollection | None = self._ports_in["InputTables"].data
        token: PortTypeHint.Token | None = self._ports_in["InputToken"].data

        if tables is None or token is None or token[0] is None or token[1] is None:
            self._ports_out["OutputTables"].data = None
            return None

        tables = (
            tables.copy()
        )  # Copy the input tables to avoid modifying the original tables
        bore_table = tables.get_table("bore_table")
        if bore_table is None or bore_table.empty:
            warnings.warn(
                "'bore_table' is not found or empty in the input table collection, the input tables will not be changed."
            )
            self._ports_out["OutputTables"].data = tables
            return tables

        if "bore_type" not in bore_table.field_names:
            warnings.warn(
                "'bore_type' column not found in the 'bore_table', the input tables will not be changed."
            )
            self._ports_out["OutputTables"].data = tables
            return tables

        bore_types_mapping = get_bore_type_mapping(user_token=token[0], pid=token[1])

        bore_table["bore_type"] = bore_table["bore_type"].map(bore_types_mapping)

        self._ports_out["OutputTables"].data = tables
        return tables


@status_manage
class ExportGeoSectionByProfile1D(PipeModule):
    """Export Geo cross-section for CAD drawing by Bores only.

    注意事项：
    1、根据输入的MultiProfile1D对象，以及多条剖面线的信息，导出剖面信息
    2、实例化后execute函数不会自动执行
    """

    def __init__(
        self,
        mname="ExportGeoSectionByProfile1D",
        auto_run: bool = False,
        # bores: PortTypeHint.VerticalBores | None = None, file: str | None = None,
        bores: PortTypeHint.MultiProfile1D = None,
        file: str | None = None,
        material_table: PortTypeHint.MaterialTable = None,
        #  gwt_points: PortTypeHint.SurfacePoints | None = None,
        use_layer_num: bool = True,
        section_line_dict: Dict[str, list[str]] = None,
    ) -> None:
        """Initialize ExportGeoSectionByBores object.

        Parameters
        ----------
        section_line: 定义了每条剖面线上包含了哪些钻孔。
                          示例：{
                                    "1-1": ["zk1", "zk2", "zk3"],
                                    "2-2": ["zk4", "zk5"]
                                }

        Ports
        -----
        InputMultiProfile1D: 入端口1，接收MultiProfile1D对象

        InputMateiralTable: 入端口2，接收MateiralTable对象

        OutputSectionForCadDraw: 出端口，输出SectionForCadDraw对象

        """
        super().__init__(mname=mname, auto_run=auto_run)

        self._ports_in = {
            "InputMultiProfile1D": Port(
                ptype=PortType.MultiProfile1D,
                data=bores,
                pdoc="The uniform vertical bores to create geo cross-section.",
            ),
            "InputMaterialTable": Port(
                ptype=PortType.MaterialTable,
                data=material_table,
                pdoc="The materials information used for CAD drawing.",
            ),
            #   "InputWaterPoints": Port(ptype=PortType.SurfacePoints,data=gwt_points,
            #                            pdoc="The surface points to create water table line.")
        }

        self._ports_out = {
            "OutputSectionForCadDraw": Port(
                ptype=PortType.GeoSections, pdoc="The SectionForCadDraw output. "
            )
        }

        self.file = file
        self.section_lines = section_line_dict
        self.use_layer_num = use_layer_num
        self.decimal = 3
        self.pinch_type = 1  # 孔上尖灭
        self.eps = 0.01
        self.merge = False  # 封闭区域是否要合并，即需不需要使用钻孔线分割

        self.executed = True
        if self.auto_run:
            self.execute()

    @property
    def InputMultiProfile1D(self) -> None:
        raise AttributeError("Property 'InputBores' is write-only.")

    @InputMultiProfile1D.setter
    def InputMultiProfile1D(self, value: PortTypeHint.MultiProfile1D | None) -> None:
        self["InputMultiProfile1D"] = value

    # @property
    # def InputWaterPoints(self) -> None:
    #     raise AttributeError("Property 'InputWaterPoints' is write-only.")

    # @InputWaterPoints.setter
    # def InputWaterPoints(self,value: PortTypeHint.SurfacePoints | None) -> None:
    #     self["InputWaterPoints"] = value

    @property
    def OutputSectionForCadDraw(self) -> PortTypeHint.GeoSections | None:
        return self._ports_out["OutputSectionForCadDraw"].data

    def set_cal_params(self, reset: bool = True) -> None:
        return None

    def execute(self) -> PortTypeHint.GeoSections | None:
        # vbores: PortTypeHint.VerticalBores | None = self._ports_in["InputBores"].data
        vbores: PortTypeHint.MultiProfile1D = self._ports_in["InputMultiProfile1D"].data
        material_table: PortTypeHint.MaterialTable = self._ports_in[
            "InputMaterialTable"
        ].data
        # gwt_points: PortTypeHint.SurfacePoints | None = self._ports_in["InputWaterPoints"].data

        if vbores is None:
            print("输入的钻孔数据为空")
            return None

        # section_bores_dict = self.section_lines
        # 将剖面线信息转为字典
        if self.section_lines is not None:
            section_bores_dict = self.section_lines
            # section_bores_dict = self.section_lines
            # print('剖面字典为：')
            # print(section_bores_dict)
        else:
            # 生成gdiWarning警告
            warnings.warn(f"没有剖面线信息", GDIWarning)
            print("没有剖面线信息")
            return None

        sections_dic = {}
        for k, v in section_bores_dict.items():
            section_name = k
            bores_name = v  # list型

            sections_dic[section_name] = {}

            # 将该条剖面线的钻孔信息筛选出来
            # print('输入的全部钻孔数据如下')
            # print(vbores.bores_table)
            # print(vbores.layers_table)

            bores_table = vbores.profiles_table  # dataframe
            layers_table = vbores.layers_table  # 注意vbores.layers_table没有地层编号列
            # 增加地层编号列。layers_table中列materials_id等于material_table的索引来获取material_table中列layer_number的值
            layers_table["layers_num"] = layers_table["materials_id"].map(
                material_table["layer_number"]
            )

            # 将列名pnum改为num
            bores_table.rename(columns={"pnum": "num"}, inplace=True)
            layers_table.rename(columns={"pnum": "num"}, inplace=True)

            # 筛选需要的数据
            bores_table_section = bores_table[bores_table["num"].isin(bores_name)]
            layers_table_section = layers_table[layers_table["num"].isin(bores_name)]

            # print('剖面'+ section_name +'的数据如下：')
            # print(bores_table_section)
            # print(layers_table_section)

            # 新建verticalbores对象
            section_vbores = VerticalBores(
                bores_table=bores_table_section, layers_table=layers_table_section
            )
            section_uni_vbores = UniVerticalBores(section_vbores)
            section_uni_vbores.run(
                use_layer_num=self.use_layer_num
            )  # 执行run函数才能生成hierarchys_table

            # *************************** 对每一条线的剖面钻孔进行操作 *******************************

            # 生成boring_set_uniform_unsorted
            hierarchys_table = section_uni_vbores.hierarchys_table
            # bore_hierarchys_table = pd.DataFrame(data=data,columns=["num","hierarchys_depth","hierarchys_material_id",
            #                                                         "hierarchys_id"])
            bores_table = section_uni_vbores.bores_table
            # bores_table = pd.DataFrame(columns=["num","top","x_coord","y_coord"])
            layers_table = section_uni_vbores.layers_table
            # layers_table = pd.DataFrame(columns=["num","layers_depth","materials_id","layers_num","materials"])
            # materials_name = section_uni_vbores.materials_name

            # print('本次包含的钻孔数据如下：')
            # print(materials_name)
            # print(bores_table)
            # print(layers_table)
            # print(hierarchys_table)

            boring_set_uniform = []  # 统一化之后的钻孔列表
            boring_set_checked = []  # 未统一化的钻孔列表
            for index, row in bores_table.iterrows():  # 迭代钻孔表的每一行
                boring = PreProfile1D()
                uniboring = PreProfile1D()

                boring.id = row["num"]
                boring.elevation_top = row["top"]
                boring.x = row["x_coord"]
                boring.y = row["y_coord"]
                boring.layers = []

                uniboring.id = row["num"]
                uniboring.elevation_top = row["top"]
                uniboring.x = row["x_coord"]
                uniboring.y = row["y_coord"]
                uniboring.layers = []

                boring_set_checked.append(boring)
                boring_set_uniform.append(uniboring)

            # 按照bores_name里面的钻孔编号重新给boring_set_checked排序
            boring_set_checked = sorted(
                boring_set_checked, key=lambda x: bores_name.index(x.id)
            )
            boring_set_uniform = sorted(
                boring_set_uniform, key=lambda x: bores_name.index(x.id)
            )

            # 给初始钻孔每一层赋数据
            last_num = ""
            last_bottom_depth = 0
            for index, row in layers_table.iterrows():  # 迭代层序表的每一行
                num = row["num"]  # 钻孔编号
                # print('当前地层的钻孔编号为：'+num)
                boring = find_node_by_id(num, boring_set_checked)
                if boring is None:
                    print("地层没有找到对应的钻孔")
                layer = {}
                if num != last_num:  # 新的钻孔编号
                    layer["depth_top"] = 0
                else:  # 否则等于上一层底深
                    layer["depth_top"] = last_bottom_depth

                if self.use_layer_num:  # 使用地层编号
                    layer_num = row[
                        "layers_num"
                    ]  # Gmodel可能会没有该列信息，但是统一化的时候，不能没有layer_code,因此采用1+materials_id的形式
                else:  # 没有地层编号
                    layer_num = "1-" + str(row["materials_id"])

                layer["layer_code"] = layer_num
                layer["depth_bottom"] = row["layers_depth"]
                # layer['material_name'] = row['materials'] #Gmodel中这一列数据可能没有传
                material_id = row["materials_id"]
                layer["material_id"] = material_id

                material_name = section_uni_vbores.materials_name.get(material_id, "")
                layer["material_name"] = (
                    material_name  # 默认为地层编号。注意gmodel中，layers_table可能没有materials数据
                )

                boring.layers.append(layer)
                last_num = num
                last_bottom_depth = row["layers_depth"]

            # 给统一化钻孔每一层赋数据
            last_num = ""
            last_bottom_depth = 0
            for index, row in hierarchys_table.iterrows():  # 迭代层序表的每一行
                num = row["num"]
                boring = find_node_by_id(num, boring_set_uniform)
                layer = {}
                if num != last_num:  # 新的钻孔编号
                    layer["depth_top"] = 0
                else:  # 否则等于上一层底深
                    layer["depth_top"] = last_bottom_depth

                material_id = row["hierarchys_material_id"]
                material_name = section_uni_vbores.materials_name.get(material_id, "")
                layer["material_name"] = material_name  # 默认为地层编号

                layer["layer_code"] = (
                    None  # 地层编号, hierarchys_table表没有统一化层的地层编号信息
                )
                layer["material_id"] = row["hierarchys_material_id"]
                depth = row["hierarchys_depth"]
                if depth == -1:
                    layer["depth_bottom"] = last_bottom_depth
                else:
                    layer["depth_bottom"] = row["hierarchys_depth"]

                last_num = num
                last_bottom_depth = layer["depth_bottom"]
                boring.layers.append(layer)

            # for bore in boring_set_checked:  # 按照原始的钻孔顺序重排
            #     boring = find_node_by_id(bore.id,boring_set_uniform_unsorted)
            #     boring_set_uniform.append(boring)

            # ************************** 生成二维剖面 孔上尖灭  ***********************************************************************

            if self.pinch_type == 1:
                # 1、钻孔线。将钻孔作为一维剖面投射到xy平面画图。按x轴正方向排列钻孔，第一个钻孔的x为0，其余按照距离依次排列。y坐标按照每个钻孔层面的高程来赋值
                x_profile_last = 0.0
                distance = 0.0
                boring_line_list = (
                    []
                )  # 原始钻孔线在二维平面的投影线列表。元素为每个地层分界点的三维坐标，包括了钻孔顶点。
                material_point_list = (
                    []
                )  # 将初始钻孔分层点上移，形成岩性点列表，用于判断封闭多边形的材料属性。不包括钻孔顶点
                line_point_list = (
                    []
                )  # 平面上钻孔形成的里程线。元素为钻孔的xy坐标。顺序按照bores_info.json里面的钻孔顺序依次连接。

                # 初始钻孔线生成：获取未统一化钻孔的分层点数据，并投射到二维平面
                for i, boring in enumerate(boring_set_checked):
                    interface_point_list = []  # 每个钻孔分层点列表
                    top = boring.elevation_top
                    # print('初始化孔编号：'+boring.id)
                    # print('初始化孔的孔顶高程为：' + str(top))
                    # 第一个点为钻孔顶点
                    if i == 0:
                        x_profile = 0
                        line_point = (boring.x, boring.y)
                        line_point_list.append(line_point)  # 用于计算里程
                    else:
                        line_point = (boring.x, boring.y)
                        line_point_list.append(line_point)

                        line_shp = LineString(line_point_list)
                        x_profile = round(line_shp.length, self.decimal)

                    y_profile = top
                    interface_point_list.append((x_profile, y_profile))
                    # 后续每个层的底点
                    for i, layer in enumerate(boring.layers):
                        x_profile = x_profile
                        # y_profile = round((top - layer['depth_bottom']),2)
                        y_profile = top - layer["depth_bottom"]
                        interface_point_list.append((x_profile, y_profile))

                        y_middle_profile = (
                            y_profile + (layer["depth_bottom"] - layer["depth_top"]) / 2
                        )  # 把层底点上移层厚的一半，作为岩性判别点
                        material_point_dic = {}
                        material_point_dic["coords_profile"] = (
                            x_profile,
                            y_middle_profile,
                        )  # 剖面上的坐标定义为一个元组
                        material_point_dic["material_name"] = (
                            layer["layer_code"] + layer["material_name"]
                        )  # 地层编号加岩性名称的组合方式
                        material_point_list.append(material_point_dic)

                    boring_line_list.append(interface_point_list)

                    last_x = boring.x
                    last_y = boring.y
                    x_profile_last = x_profile

                # 2、地层线。
                interface_line_material_list = []  # 初始地层线的材料名称列表
                x_profile_last = 0.0
                distance = 0.0
                uniboring_line_list = (
                    []
                )  # 统一化之后的钻孔线，元素为钻孔分层点三维坐标。注意只包含每层的底点，不包含钻孔顶点。
                terrain_line = []  #
                line_point_list = []

                for i, boring in enumerate(boring_set_uniform):

                    uniboring_line_list.append(
                        []
                    )  # uniboring_line_list每个元素保存的是统一化后每个钻孔的分层点。每个列表里面的点个数是相同的。
                    # 第一个点为钻孔顶点
                    # print('统一化孔编号：' + boring.id)
                    top = boring.elevation_top
                    # print('统一化孔的孔顶高程为：'+str(top))

                    # profile_num = profile_num + '_' + boring.id #todo: 该行需要注释

                    # 第一个点为钻孔顶点
                    if i == 0:
                        x_profile = 0
                        line_point = (boring.x, boring.y)
                        line_point_list.append(line_point)
                    else:
                        # dis = math.sqrt((boring.x - last_x)**2 + (boring.y - last_y)**2)
                        # distance = round(dis,5)
                        # x_profile = round((x_profile_last + dis),5)
                        line_point = (boring.x, boring.y)
                        line_point_list.append(line_point)

                        line_shp = LineString(line_point_list)
                        x_profile = round(line_shp.length, self.decimal)

                    y_profile = top

                    terrain_line.append(
                        (x_profile, y_profile)
                    )  # 第一条分层线的第一个点作为地形线的第一个点

                    for layer in boring.layers:
                        x_profile = x_profile
                        y_profile = top - layer["depth_bottom"]
                        # y_profile = round((top - layer['depth_bottom']),2)
                        uniboring_line_list[i].append((x_profile, y_profile))
                        # interface_line_material_list.append(layer['layer_code']) #todo：地层线的信息换成岩性名称
                        interface_line_material_list.append(
                            layer["material_id"]
                        )  # todo：地层线的信息换成材料id
                    last_x = boring.x
                    last_y = boring.y
                    x_profile_last = x_profile

                # print('钻孔线')
                # print(boring_line_list)
                # print('统一化后钻孔线') #注意统一化钻孔线没有加上地表点
                # print(uniboring_line_list)

                # *************************** 重新生成地层连线 符合剖面图规范 **************************************
                # 获取钻孔底点
                bottom_point_list = []
                for line in boring_line_list:
                    point = line[
                        -1:
                    ]  # 提取最后一个点坐标,注意切片之后仍然是一个列表，而不是里面的元素
                    bottom_point_list.append(point[0])

                # 每次处理统一化钻孔线中的一对钻孔连线
                interface_line_dic_list = []  #
                for i in range(len(uniboring_line_list) - 1):
                    # 处理每两个钻孔之间的连线
                    line_left = uniboring_line_list[i]
                    line_right = uniboring_line_list[i + 1]
                    bottom_point_left = bottom_point_list[i]
                    bottom_point_right = bottom_point_list[i + 1]

                    boring_left = boring_set_uniform[i]
                    boring_right = boring_set_uniform[i + 1]

                    bottom_touch_flag_left = False  # 触底标志
                    bottom_touch_flag_right = False

                    last_closed_point = (
                        (line_left[0][0] + line_right[0][0]) / 2,
                        (line_left[0][1] + line_right[0][1]) / 2,
                    )  # 用于封闭底部虚线
                    # 从上至下依次取地层点
                    for j in range(len(line_left)):
                        point_left = line_left[j]
                        point_right = line_right[j]
                        if j >= 1:
                            last_point_left = line_left[j - 1]
                            last_point_right = line_right[j - 1]

                        layer_thickness_left = (
                            boring_left.layers[j]["depth_bottom"]
                            - boring_left.layers[j]["depth_top"]
                        )
                        layer_thickness_right = (
                            boring_right.layers[j]["depth_bottom"]
                            - boring_right.layers[j]["depth_top"]
                        )
                        # 开始进行逻辑判断
                        # 1、两个钻孔的层序层厚度都不为0时，才能直连。分为两种情况，1）：没同时触底，则实线直连；2）：同时触底（就是最后一层）虚线直连
                        # 2、两个钻孔的层序层厚度为0时，该条线断不连
                        # 3、两个钻孔层厚度不同时为0时：1）两个分界点都没有触底，则实线直连；2）左侧触底，右侧向左平推或连中点；3）右侧触底，左侧向右平推或者连中点
                        if (
                            layer_thickness_left != 0 and layer_thickness_right != 0
                        ):  # 两层厚度都不为0
                            if (
                                point_left == bottom_point_left
                                and point_right == bottom_point_right
                            ):  # 同时触底
                                interface_line_dic = {}
                                interface_line_dic["points"] = [
                                    point_left,
                                    point_right,
                                ]  # 直接连线
                                interface_line_dic["material"] = (
                                    interface_line_material_list[j]
                                )
                                interface_line_dic["type"] = "virtual_interface"
                                interface_line_dic["geo_id"] = j
                                interface_line_dic_list.append(interface_line_dic)
                            elif (
                                point_left == bottom_point_left
                            ):  # 左线触底,直接连线，左半为虚线，右半为实线
                                interface_line_dic = {}
                                point_middle = (
                                    (point_left[0] + point_right[0]) / 2,
                                    (point_left[1] + point_right[1]) / 2,
                                )
                                interface_line_dic["points"] = [
                                    point_left,
                                    point_middle,
                                ]  #
                                interface_line_dic["material"] = (
                                    interface_line_material_list[j]
                                )
                                interface_line_dic["type"] = "virtual_interface"
                                interface_line_dic["geo_id"] = j
                                interface_line_dic_list.append(interface_line_dic)

                                interface_line_dic = {}
                                interface_line_dic["points"] = [
                                    point_middle,
                                    point_right,
                                ]  #
                                interface_line_dic["material"] = (
                                    interface_line_material_list[j]
                                )
                                interface_line_dic["type"] = "bottom_interface"
                                interface_line_dic["geo_id"] = j
                                interface_line_dic_list.append(interface_line_dic)
                            elif (
                                point_right == bottom_point_right
                            ):  # 右线触底,直接连线，右为虚线，左半为实线
                                interface_line_dic = {}
                                point_middle = (
                                    (point_left[0] + point_right[0]) / 2,
                                    (point_left[1] + point_right[1]) / 2,
                                )

                                interface_line_dic["points"] = [
                                    point_left,
                                    point_middle,
                                ]  #
                                interface_line_dic["material"] = (
                                    interface_line_material_list[j]
                                )
                                interface_line_dic["type"] = "bottom_interface"
                                interface_line_dic["geo_id"] = j
                                interface_line_dic_list.append(interface_line_dic)

                                interface_line_dic = {}
                                interface_line_dic["points"] = [
                                    point_middle,
                                    point_right,
                                ]  #
                                interface_line_dic["material"] = (
                                    interface_line_material_list[j]
                                )
                                interface_line_dic["type"] = "virtual_interface"
                                interface_line_dic["geo_id"] = j
                                interface_line_dic_list.append(interface_line_dic)
                            else:  # 都没触底，直接实线相连
                                interface_line_dic = {}
                                interface_line_dic["points"] = [
                                    point_left,
                                    point_right,
                                ]  # 直接实线连线
                                interface_line_dic["material"] = (
                                    interface_line_material_list[j]
                                )
                                interface_line_dic["type"] = "bottom_interface"
                                interface_line_dic["geo_id"] = j
                                interface_line_dic_list.append(interface_line_dic)

                            last_closed_point = (
                                (point_left[0] + point_right[0]) / 2,
                                (point_left[1] + point_right[1]) / 2,
                            )  # 为向上的封闭虚线的终点

                        elif (
                            layer_thickness_left == 0 and layer_thickness_right == 0
                        ):  # 两层厚度都为0
                            continue

                        else:  # 有且仅有其中一层厚度为0
                            # print('出现只有一侧厚度为0的情况！')
                            if (
                                bottom_touch_flag_left == False
                                and bottom_touch_flag_right == False
                            ):  # 两边都没有触底过
                                if (
                                    point_left == bottom_point_left
                                    and point_right != bottom_point_right
                                ):  # 左边触底,则左半边虚线
                                    interface_line_dic = {}
                                    point_middle = (
                                        (point_left[0] + point_right[0]) / 2,
                                        (point_left[1] + point_right[1]) / 2,
                                    )

                                    interface_line_dic["points"] = [
                                        point_left,
                                        point_middle,
                                    ]  # 左半虚线
                                    interface_line_dic["material"] = (
                                        interface_line_material_list[j]
                                    )
                                    interface_line_dic["type"] = "virtual_interface"
                                    interface_line_dic["geo_id"] = j
                                    interface_line_dic_list.append(interface_line_dic)

                                    interface_line_dic = {}
                                    interface_line_dic["points"] = [
                                        point_middle,
                                        point_right,
                                    ]  # 右半实线
                                    interface_line_dic["material"] = (
                                        interface_line_material_list[j]
                                    )
                                    interface_line_dic["type"] = "bottom_interface"
                                    interface_line_dic["geo_id"] = j
                                    interface_line_dic_list.append(interface_line_dic)
                                elif (
                                    point_left != bottom_point_left
                                    and point_right == bottom_point_right
                                ):  # 右边触底,则右半边虚线
                                    interface_line_dic = {}
                                    point_middle = (
                                        (point_left[0] + point_right[0]) / 2,
                                        (point_left[1] + point_right[1]) / 2,
                                    )

                                    interface_line_dic["points"] = [
                                        point_left,
                                        point_middle,
                                    ]  # 左半实线
                                    interface_line_dic["material"] = (
                                        interface_line_material_list[j]
                                    )
                                    interface_line_dic["type"] = "bottom_interface"
                                    interface_line_dic["geo_id"] = j
                                    interface_line_dic_list.append(interface_line_dic)

                                    interface_line_dic = {}
                                    interface_line_dic["points"] = [
                                        point_middle,
                                        point_right,
                                    ]  # 右半虚线
                                    interface_line_dic["material"] = (
                                        interface_line_material_list[j]
                                    )
                                    interface_line_dic["type"] = "virtual_interface"
                                    interface_line_dic["geo_id"] = j
                                    interface_line_dic_list.append(interface_line_dic)
                                elif (
                                    point_left != bottom_point_left
                                    and point_right != bottom_point_right
                                ):  # 都没触底，直接实线相连
                                    interface_line_dic = {}
                                    interface_line_dic["points"] = [
                                        point_left,
                                        point_right,
                                    ]  # 直接实线连线
                                    interface_line_dic["material"] = (
                                        interface_line_material_list[j]
                                    )
                                    interface_line_dic["type"] = "bottom_interface"
                                    interface_line_dic["geo_id"] = j
                                    interface_line_dic_list.append(interface_line_dic)

                                last_closed_point = (
                                    (point_left[0] + point_right[0]) / 2,
                                    (point_left[1] + point_right[1]) / 2,
                                )  # 为向上的封闭虚线的终点

                            else:  # 至少有一个点已经触底过
                                # print('出现只有一侧厚度为0，且至少有一个侧触底的情况')
                                if (
                                    layer_thickness_left != 0
                                    and layer_thickness_right == 0
                                ):  # 左边厚度不为0，则从左向右推一半，然后判断平推还是连中点，是否是虚线
                                    # print('左边厚度不为0，右边厚度为0')
                                    last_line_middle_y = (
                                        line_left[j - 1][1] + line_right[j - 1][1]
                                    ) / 2  # 上一条线中点的y坐标
                                    # 按照两种情况处理，左边平推线端点的坐标是否高于上一条线的中点
                                    if (
                                        point_left[1] <= last_line_middle_y
                                    ):  # 对比点的y坐标。平推线端点低于上一条线的中点，直接平推一半
                                        interface_line_dic = {}
                                        point_middle = (
                                            (point_left[0] + point_right[0]) / 2,
                                            point_left[1],
                                        )

                                        interface_line_dic["points"] = [
                                            point_left,
                                            point_middle,
                                        ]
                                        interface_line_dic["material"] = (
                                            interface_line_material_list[j]
                                        )
                                        interface_line_dic["geo_id"] = j
                                        if (
                                            point_left == bottom_point_left
                                        ):  # 是否触底：
                                            interface_line_dic["type"] = (
                                                "virtual_interface"
                                            )
                                        else:
                                            interface_line_dic["type"] = (
                                                "bottom_interface"
                                            )
                                        interface_line_dic_list.append(
                                            interface_line_dic
                                        )

                                        # 平推完之后，还得要加一条向上的虚拟线，构成封闭区域。虚线的上端点连接至last_closed_point
                                        interface_line_dic = {}
                                        interface_line_dic["points"] = [
                                            point_middle,
                                            last_closed_point,
                                        ]
                                        interface_line_dic["material"] = (
                                            interface_line_material_list[j]
                                        )
                                        interface_line_dic["geo_id"] = j
                                        interface_line_dic["type"] = "virtual_interface"
                                        interface_line_dic_list.append(
                                            interface_line_dic
                                        )

                                        last_closed_point = point_middle  # 设置下一条虚线的连接点（如果需要的话）

                                    else:  # 对比点的y坐标。右侧平推线端点高于于上一条线的中点，连接到中点
                                        interface_line_dic = {}
                                        point_middle = (
                                            (point_left[0] + point_right[0]) / 2,
                                            (point_left[1] + point_right[1]) / 2,
                                        )

                                        interface_line_dic["points"] = [
                                            point_left,
                                            point_middle,
                                        ]
                                        interface_line_dic["material"] = (
                                            interface_line_material_list[j]
                                        )
                                        interface_line_dic["geo_id"] = j
                                        if (
                                            point_left == bottom_point_left
                                        ):  # 是否触底：
                                            interface_line_dic["type"] = (
                                                "virtual_interface"
                                            )
                                        else:
                                            interface_line_dic["type"] = (
                                                "bottom_interface"
                                            )
                                        interface_line_dic_list.append(
                                            interface_line_dic
                                        )

                                        # 平推完之后，还得要加一条向上的虚拟线，构成封闭区域。
                                        interface_line_dic = {}
                                        interface_line_dic["points"] = [
                                            point_middle,
                                            last_closed_point,
                                        ]
                                        interface_line_dic["material"] = (
                                            interface_line_material_list[j]
                                        )
                                        interface_line_dic["geo_id"] = j
                                        interface_line_dic["type"] = "virtual_interface"
                                        interface_line_dic_list.append(
                                            interface_line_dic
                                        )

                                        last_closed_point = point_middle  # 设置下一条虚线的连接点（如果需要的话）

                                elif (
                                    layer_thickness_left == 0
                                    and layer_thickness_right != 0
                                ):  # 右边厚度不为0，则从右向左推一半，然后判断平推还是连中点，是否是虚线
                                    last_line_middle_y = (
                                        line_left[j - 1][1] + line_right[j - 1][1]
                                    ) / 2  # 上一条线中点的y坐标
                                    # 按照两种情况处理，左边平推线端点的坐标是否高于上一条线的中点
                                    if (
                                        point_right[1] <= last_line_middle_y
                                    ):  # 对比点的y坐标。平推线端点低于上一条线的中点，直接平推一半
                                        interface_line_dic = {}
                                        point_middle = (
                                            (point_left[0] + point_right[0]) / 2,
                                            point_right[1],
                                        )

                                        interface_line_dic["points"] = [
                                            point_middle,
                                            point_right,
                                        ]
                                        interface_line_dic["material"] = (
                                            interface_line_material_list[j]
                                        )
                                        interface_line_dic["geo_id"] = j
                                        if (
                                            point_right == bottom_point_right
                                        ):  # 是否触底：
                                            interface_line_dic["type"] = (
                                                "virtual_interface"
                                            )
                                        else:
                                            interface_line_dic["type"] = (
                                                "bottom_interface"
                                            )
                                        interface_line_dic_list.append(
                                            interface_line_dic
                                        )

                                        # 平推完之后，还得要加一条向上的虚拟线，构成封闭区域。
                                        interface_line_dic = {}
                                        interface_line_dic["points"] = [
                                            point_middle,
                                            last_closed_point,
                                        ]
                                        interface_line_dic["material"] = (
                                            interface_line_material_list[j]
                                        )
                                        interface_line_dic["geo_id"] = j
                                        interface_line_dic["type"] = "virtual_interface"
                                        interface_line_dic_list.append(
                                            interface_line_dic
                                        )

                                        last_closed_point = point_middle  # 设置下一条虚线的连接点（如果需要的话）

                                    else:  # 对比点的y坐标。右侧平推线端点高于于上一条线的中点，连接到中点
                                        interface_line_dic = {}
                                        point_middle = (
                                            (point_left[0] + point_right[0]) / 2,
                                            (point_left[1] + point_right[1]) / 2,
                                        )

                                        interface_line_dic["points"] = [
                                            point_middle,
                                            point_right,
                                        ]
                                        interface_line_dic["material"] = (
                                            interface_line_material_list[j]
                                        )
                                        interface_line_dic["geo_id"] = j
                                        if (
                                            point_right == bottom_point_right
                                        ):  # 是否触底：
                                            interface_line_dic["type"] = (
                                                "virtual_interface"
                                            )
                                        else:
                                            interface_line_dic["type"] = (
                                                "bottom_interface"
                                            )
                                        interface_line_dic_list.append(
                                            interface_line_dic
                                        )

                                        # 平推完之后，还得要加一条向上的虚拟线，构成封闭区域。
                                        interface_line_dic = {}
                                        interface_line_dic["points"] = [
                                            point_middle,
                                            last_closed_point,
                                        ]
                                        interface_line_dic["material"] = (
                                            interface_line_material_list[j]
                                        )
                                        interface_line_dic["geo_id"] = j
                                        interface_line_dic["type"] = "virtual_interface"
                                        interface_line_dic_list.append(
                                            interface_line_dic
                                        )

                                        last_closed_point = point_middle  # 设置下一条虚线的连接点（如果需要的话）

                        # 设置触底标志
                        if point_left == bottom_point_left:
                            bottom_touch_flag_left = True
                        if point_right == bottom_point_right:
                            bottom_touch_flag_right = True

                # *************************** 重新生成地层连线  结束 **************************************
                # print('仅地层线：')
                layer_lines = []
                v_layer_lines = []
                for interface_line_dic in interface_line_dic_list:
                    if interface_line_dic["type"] != "virtual_interface":
                        layer_lines.append(interface_line_dic["points"])
                    else:
                        v_layer_lines.append(interface_line_dic["points"])
                # print('实心地层线：')
                # print(layer_lines)
                # print('虚拟地层线：')
                # print(v_layer_lines)

                # 生成整体ploygon的底边范围线。主要思路是把所有虚拟线连起来，有部分虚拟线两个端点的x坐标相同，只取最下方的那天虚拟线

                bottom_virtual_interface_line_list = []  # 虚拟线列表
                for dic in interface_line_dic_list:
                    if dic["type"] == "virtual_interface":
                        bottom_virtual_interface_line_list.append(
                            dic["points"]
                        )  # 注意值的形式为[(0,0),(1,1)]

                interface_line_segment_list = []
                # 所有线段放在一起
                for line_segment in interface_line_dic_list:
                    interface_line_segment_list.append(line_segment["points"])

                buffer_line_list = []  # 用于地层线加钻孔线一起寻找封闭区域
                buffer_line_list.extend(interface_line_segment_list)

                inner_boring_line_list = []  # 除两侧外的钻孔线
                for i in range(len(boring_line_list)):
                    if i != 0 and i != (len(boring_line_list) - 1):  # 去除两边的钻孔线
                        inner_boring_line_list.append(boring_line_list[i])

                buffer_line_list.extend(inner_boring_line_list)

                # ******************** 封闭区域判断岩性方法2：与钻孔地层线是否相交来判断封闭区域的岩性 *******************
                # 将钻孔线拆分为带属性的线段
                boring_interval_list = []
                line_point_list = []
                for i, boring in enumerate(boring_set_checked):
                    # interface_point_list = []#每个钻孔分层点列表
                    # 第一个钻孔x坐标
                    if i == 0:
                        x_profile = 0  # 在剖面上的x坐标
                        line_point = (boring.x, boring.y)
                        line_point_list.append(line_point)
                    else:
                        # dis = math.sqrt((boring.x - last_x) ** 2 + (boring.y - last_y) ** 2)
                        # # distance = round(dis, 5)
                        # x_profile = round((x_profile_last + dis),5)
                        line_point = (boring.x, boring.y)
                        line_point_list.append(line_point)

                        line_shp = LineString(line_point_list)
                        x_profile = round(line_shp.length, self.decimal)

                    # print(x_profile)

                    boring_top_ele = boring.elevation_top  # 孔顶高程
                    top = boring_top_ele
                    for layer in boring.layers:
                        interval_dic = {}
                        bottom = boring_top_ele - layer["depth_bottom"]  # 层底高程
                        point_top = (x_profile, top)
                        point_bottom = (x_profile, bottom)

                        interval_dic["points"] = [point_top, point_bottom]
                        interval_dic["material"] = layer.get(
                            "material_id"
                        )  # todo:换成材料id。用于判断地层线、封闭区域的材料名称
                        # interval_dic['material'] = layer['material_name']#todo:换成岩性名称。

                        boring_interval_list.append(interval_dic)

                        top = bottom

                    last_x = boring.x
                    last_y = boring.y
                    x_profile_last = x_profile
                # print('共有初始地层数量：' + str(len(boring_interval_list)))

                # ********************* 通过求非交区域获取各个封闭岩性区域 ****************************
                # 制作用于寻找封闭岩性区域的外轮廓
                side_line_list = []  # 仅保留两边的钻孔线
                side_line_list.append(boring_line_list[0])
                side_line_list.append(boring_line_list[len(boring_line_list) - 1])
                bottom_lines = bottom_virtual_interface_line_list
                area_line_list = []  # 最大轮廓线
                area_line_list.extend(side_line_list)

                # ******************************** 通过地形点生成剖面图地形线 开始 ***************************************
                # ******************************** 通过地形点生成剖面图地形线 结束 ***************************************

                area_line_list.append(terrain_line)  # 用于生成最大的轮廓线
                area_line_list.extend(
                    bottom_virtual_interface_line_list
                )  # 用于生成最大的轮廓线

                # 由于area_line_list过于复杂，生成的polygon（目标搜索区域）经常出问题，因此重新采用convex_hull作为搜索区域的方案来生成整个分割区域,通过封闭区域是否和钻孔线接触来返回目标的封闭区域。
                area_point_list = []  # 外轮廓点列表
                for line in area_line_list:
                    for p in line:
                        area_point_list.append(p)

                polygon_convex = convex_hull(MultiPoint(area_point_list))  # 外轮廓

                # 目标区域的分割线
                # eps = float(args.eps)  # 分割线外扩距离
                interface_line_segment_list.append(
                    terrain_line
                )  # todo:extend改为append
                buffer_line_list.append(terrain_line)
                if self.merge:  # 分割线外扩，目的是使用difference函数
                    # print('所有地层线+地表线')
                    # print(interface_line_segment_list)
                    buffer = MultiLineString(interface_line_segment_list).buffer(
                        self.eps, join_style="mitre"
                    )  # 所有分界线外扩一个极小范围
                else:
                    buffer = MultiLineString(buffer_line_list).buffer(
                        self.eps, join_style="mitre"
                    )  # 默认：所有分界线（加钻孔线）外扩一个极小范围

                # 通过求取目标区域（外轮廓范围）与分割线的不相交区域来获取封闭区域
                valid_polygon_list = []  # 满足要求的封闭区域
                invalid_polygon_list = []  # 不满足要求的封闭区域
                if polygon_convex != None:
                    diff = polygon_convex.difference(buffer)
                    if len(diff.geoms) == 0:
                        print("没找到任何封闭区域，请检查地形数据！")
                    else:
                        block_polygons = [
                            geom for geom in diff.geoms
                        ]  # 所有封闭区域集合。如果diff.geoms为空，需要进行一个异常处理
                        # print('封闭区域个数：')
                        # print(len(block_polygons))
                        # 确定每个封闭区域的材料名称
                        # color_list = [RED,YELLOW,BLACK,BLUE,GRAY,GREEN,DARKGRAY]
                        polygon_dic_list = (
                            []
                        )  # 用于存储合法封闭区域相关数据，并转为二维剖面json文件

                        for i, poly in enumerate(block_polygons):
                            # 封闭区域交点坐标round处理
                            point_round_list = []
                            for p in list(poly.exterior.coords):
                                s = round(p[0], self.decimal)
                                z = round(p[1], self.decimal)
                                p_round = (s, z)
                                point_round_list.append(p_round)
                            poly_round = Polygon(point_round_list)

                            mat_list = []
                            # poly_buff = poly.buffer(eps, join_style='mitre')
                            # poly_buff = poly.buffer(eps*2, join_style='mitre')
                            polygon_dic = {}

                            for interval in boring_interval_list:
                                line_buf = LineString(interval["points"]).buffer(
                                    self.eps, cap_style="flat"
                                )  # 经检查，buffer有问题，左右扩展的数值有微小差异
                                point_round_list = []
                                for p in list(line_buf.exterior.coords):
                                    s = round(p[0], self.decimal)
                                    z = round(p[1], self.decimal)
                                    p_round = (s, z)
                                    point_round_list.append(p_round)
                                line_buf_round = Polygon(point_round_list)

                                if poly_round.intersects(
                                    line_buf_round
                                ):  # 判断是否有交集
                                    mat_list.append(interval["material"])

                            mat_set = set(mat_list)  # 一共包含几种岩性
                            if len(mat_set) == 1:
                                polygon_dic["material"] = list(mat_set)[0]
                                polygon_dic["type"] = "closed_area"
                                polygon_dic["points"] = list(poly_round.exterior.coords)
                                valid_polygon_list.append(poly_round)
                                polygon_dic_list.append(polygon_dic)
                            else:
                                # print('封闭区域未找到岩性。。。')
                                # print(mat_set)
                                polygon_dic["material"] = "unknown"
                                polygon_dic["type"] = "closed_area"
                                polygon_dic["points"] = list(poly_round.exterior.coords)

                                invalid_polygon_list.append(poly_round)

                            # polygon_dic_list.append(polygon_dic)#todo:只存储有岩性的封闭区域

                sections_dic[section_name]["boring_set_checked"] = boring_set_checked
                sections_dic[section_name]["terrain_line"] = terrain_line
                sections_dic[section_name][
                    "interface_line_dic_list"
                ] = interface_line_dic_list
                sections_dic[section_name]["polygon_dic_list"] = polygon_dic_list
                sections_dic[section_name]["line_point_list"] = line_point_list

        # ************************************************************************************** 导出画图对象 **********************************************************************************
        data_to_save = []
        # sections_dic = self._sections_dic
        # bores: PortTypeHint.VerticalBores | None = self._ports_in["InputBores"].data
        section_for_draw_list = []
        bores = section_vbores
        for section_name, section_info in sections_dic.items():

            section_for_draw = SectionForCadDraw()

            # ****************************** gsc文件中的bounds生成 **************************************
            # 创建一个 MultiLineString 对象
            line_list = []
            for line_dic in section_info["interface_line_dic_list"]:
                line_list.append(line_dic["points"])

            multiline = MultiLineString(line_list)

            # 使用 bounds 属性获取边界框
            bounds_shape = multiline.bounds  # 属性都会返回一个元组(minx,miny,maxx,maxy)
            # 转成SectionForCadDraw规定的格式 (xmin, xmax, ymin, ymax)
            bounds = (
                bounds_shape[0],
                bounds_shape[2],
                bounds_shape[1],
                bounds_shape[3],
            )
            # bounds = (bounds_shape[0],bounds_shape[2],bounds_shape[1],bounds_shape[3])

            section_for_draw.bounds = bounds
            # ****************************** gsc文件中的bores生成 **************************************
            # 生成gsc文件中的SectionBore
            # bore_num: str = Field(description="钻孔编号")
            # x_coord: float = Field(description="钻孔孔顶x坐标，即里程")
            # top: float = Field(description="钻孔孔顶标高")
            # elevations: Sequence[float] = Field(description="钻孔各层层底标高")
            # depths: Sequence[float] = Field(description="钻孔各层层底深度")
            # materials: Sequence[int] = Field(description="钻孔各层材料id")
            # layers_num: Sequence[str] = Field(description="钻孔各层地层编号")
            # distance_to: float = Field(description="钻孔距离剖面的最小距离")

            seciton_bores = []
            section_line = (
                []
            )  # 里面是每个钻孔的xy坐标元组，section_line = [(bore1.x,bore1.y),(bore2.x,bore2.y)]
            x_profile = 0
            # for boring in boring_set_checked:
            for i, boring in enumerate(section_info["boring_set_checked"]):
                # 第一个点为钻孔顶点
                if i == 0:
                    x_profile = 0
                    line_point = (boring.x, boring.y)
                    section_line.append(line_point)  # 用于计算里程
                else:
                    line_point = (boring.x, boring.y)
                    section_line.append(line_point)

                    line_shp = LineString(section_line)
                    x_profile = round(line_shp.length, self.decimal)
                depths = []
                for layer in boring.layers:
                    depths.append(layer["depth_bottom"])
                # section_bore.depths = depths

                bore_top = boring.elevation_top
                elevations = [bore_top - x for x in depths]

                layers_num = []
                materials = []  # 各层材料id
                for layer in boring.layers:
                    layers_num.append(layer["layer_code"])
                    mat_name = layer[
                        "layer_code"
                    ]  # todo：注意materials_name保存的是地层编号
                    for k, v in bores.materials_name.items():
                        if v == mat_name:
                            materials.append(k)
                            break

                materials_int = [int(item) for item in materials]

                section_bore = SectionBore(
                    bore_num=boring.id,
                    x_coord=x_profile,
                    top=boring.elevation_top,
                    elevations=elevations,
                    depths=depths,
                    materials=materials,
                    layers_num=layers_num,
                    distance_to=0.0,
                )

                seciton_bores.append(section_bore)

            section_for_draw.bores = seciton_bores

            # ****************************** gsc文件中的interfaces生成 **************************************
            # class SectionInterface(BaseModel):
            #     """地质剖面图上地层线信息"""
            #     coords: Sequence[Sequence[float]] = Field(description="地层线xy坐标")
            #     material: int = Field(description="材料ID，地表材料ID为-1，地下水位为-2")
            #     layer: int = Field(description="地层ID，地表地层ID为-1，地下水位为-2")

            interfaces = []

            # 地表线
            # section_interface = SectionInterface()
            # section_interface.coords = self.terrain_line
            # section_interface.material = -1
            # section_interface.layer = -1
            section_interface = SectionInterface(
                coords=section_info["terrain_line"],
                material=-1,
                layer=-1,
                virtual_interface=False,
            )
            interfaces.append(section_interface)

            # 地层线
            # interface_line_dic = {}
            # interface_line_dic['points'] = [point_middle,last_closed_point]
            # interface_line_dic['material'] = interface_line_material_list[j] #保存的是岩性名称
            # interface_line_dic['geo_id'] = j
            # interface_line_dic['type'] = 'virtual_interface'
            # interface_line_dic_list.append(interface_line_dic)

            for interface_line_dic in section_info["interface_line_dic_list"]:
                # section_interface = SectionInterface()
                # section_interface.coords = interface_line_dic['points']
                material = interface_line_dic["material"]  # 保存的是material_id
                layer_id = interface_line_dic["geo_id"]
                if interface_line_dic["type"] == "virtual_interface":
                    virtual_interface = True
                else:
                    virtual_interface = False

                section_interface = SectionInterface(
                    coords=interface_line_dic["points"],
                    material=material,
                    layer=layer_id,
                    virtual_interface=virtual_interface,
                )
                interfaces.append(section_interface)

            # 生成水位线
            # gwt_points: PortTypeHint.SurfacePoints | None = self._ports_in["InputWaterPoints"].data
            # if gwt_points is not None:
            #     points_df = gwt_points.points_table

            #     # 找到每个钻孔的水位点
            #     points = []
            #     for index,row in points_df.iterrows():
            #         x = row['x_coord']
            #         y = row['y_coord']
            #         z = row['z_coord']

            #         points.append((x,y,z))

            #     water_points_shape = MultiPoint(points)

            #     # 创建Point对象B
            #     water_point_x_profile = 0
            #     water_pointxy_list = []
            #     water_line = []
            #     for i,boring in enumerate(section_info['boring_set_checked']):
            #         water_pointxy_list.append((boring.x,boring.y))
            #         bore_top_point = Point(boring.x,boring.y,boring.elevation_top)

            #         nearest_point_A,nearest_point_B = nearest_points(water_points_shape,bore_top_point)
            #         water_point_y_profile = nearest_point_A[2]  # 水位点的z坐标

            #         line_shp = LineString(water_pointxy_list)
            #         water_point_x_profile = round(line_shp.length,self.decimal)
            #         if i == 0:
            #             water_line.append((0,water_point_y_profile))
            #         else:
            #             water_line.append((water_point_x_profile,water_point_y_profile))

            #     section_interface = SectionInterface(coords=water_line,material=-2,layer=-2)
            #     interfaces.append(section_interface)

            # section_for_draw.interfaces = interfaces

            # ****************************** gsc文件中的hateches生成 **************************************
            # hatches: list[SectionHatch] | None = Field(None,description="剖面各封闭填充区域信息")

            # class SectionHatch(BaseModel):
            #     """地质剖面图填充信息"""
            #     coords: Sequence[Sequence[float]] = Field(description="填充区域边界xy坐标")
            #     material: int = Field(description="材料ID")
            #     layer: int = Field(description="地层ID")

            hatches = []
            #     polygon_dic['material'] = list(mat_set)[0] #材料名称
            #     polygon_dic['type'] = 'closed_area'
            #     polygon_dic['points'] = list(poly_round.exterior.coords)
            #     valid_polygon_list.append(poly_round)
            for polygon_dic in section_info["polygon_dic_list"]:
                material_id = int(
                    polygon_dic["material"]
                )  # polygon_dic['material']保存的是地层的material_id信息
                section_hatch = SectionHatch(
                    coords=polygon_dic["points"], material=material_id, layer=0
                )
                hatches.append(section_hatch)

            section_for_draw.hatches = hatches

            # ****************************** gsc文件中的section_line生成 **************************************
            # section_line: Sequence[Sequence[float]] | None = Field(None,description="剖切线坐标")

            section_for_draw.section_line = section_info["line_point_list"]

            # ****************************** gsc文件中的materials生成 **************************************
            # materials = None
            # if material_table is not None:
            #     materials = {}
            #     materials_info = material_table.to_dict(orient="records")
            #     for material in materials_info:
            #         material_id = material.pop("material_id")
            #         materials[material_id] = SectionMaterial(**material)

            # section_for_draw.materials = materials

            # ****************************** gsc文件中的section_num生成 **************************************
            section_for_draw.section_num = section_name

            section_for_draw_list.append(section_for_draw)
            # save to json string
            data_to_save.append(
                (f"{section_name}.json", section_for_draw.model_dump_json())
            )

        self._ports_out["OutputSectionForCadDraw"].data = section_for_draw_list
        return section_for_draw_list
